Repository navigation
fix(minimax-bot): 回合重试从 2 次延长到 10 档(仍有上限),400/404/422 不重试 - #49
Hylouis233 wants to merge 2 commits into
Conversation
## 要解决的问题:一次 30 秒的服务端抖动就让 Bot 死掉 上游 0.7.4 的重试窗口是 `TURN_RETRY_DELAYS_MS = [8000, 20000]` —— **只重试 2 次**(等 8 秒、等 20 秒),然后就放弃。 而 Host 的 `toLLMRetryDecision()` 对**通用 5xx 故意 fail-closed**: `LLM_UPSTREAM_ERROR`(50113)是「400 及以上的兜底桶」, 把 HTTP 400/404 这种重试无意义的错误和 HTTP 500 这种**暂时性**错误混在一起, 于是分类器只能整体判成不可重试。 两条合起来的后果: 一次持续 30 秒的服务端抖动(`Internal server error (500)` / `502 Bad Gateway` / `net::ERR_NETWORK_CHANGED` / 限流 / 模型过载)就会让回合**第一次失败就死掉**, 只能靠人手点一下才回来。Bot 团队的可用性因此被一次瞬时故障决定。 ## 复现场景 1. 让 MiniMax Bot 团队里任何一个 bot 收到一条任务; 2. 在它请求模型时让上游返回一次 500(或断网、限流、网关 502); 3. 观察:该回合**只尝试了 3 次**(首次 + 2 次重试,共约 28 秒)就返回 error, bot 停在原地,需要人工再点一次。 ## 改动 ### 1. 重试窗口从 2 档延长到 10 档(**仍然有上限**) ``` 8s → 20s → 40s → 60s × 7 ``` 总窗口约 **8 分钟**,足以自愈绝大多数瞬时抖动。 - **有上限**:第 10 次之后把原始错误原样暴露给用户,不会悄悄重试到天荒地老。 - **窗口数组仍是唯一的旋钮**:注入 `retryDelaysMs: []` 依然表示「完全不重试」, 既有语义与既有测试一字未改(`engine-reliability.test.mjs` 51/51 通过)。 ### 2. 400 / 404 / 422 **不重试** 重试它们只是把同一次拒绝重复十遍。Host 自己的注释说得很清楚: *would turn an HTTP 400 or 404 into ten identical attempts*。 ### 3. 错误文本匹配收窄 只认**明确的服务端 / 网络故障形态**: ``` net::ERR_NETWORK_CHANGED / ERR_NETWORK / ECONNRESET|ECONNREFUSED|ETIMEDOUT|EPIPE socket hang up / internal server error / bad gateway|gateway timeout|service unavailable 请求处理超时 / unknown error, 520 / overloaded|rate limit|too many requests|model is busy HTTP 500 / status 502 / code=503(要带上下文,不认裸数字) ``` - **不认裸数字** ⇒「用了 400 tokens」「第 404 步」这类正常消息不会被误判成错误 - **不含 400/404/422** ⇒ 上面那条不重试的规则 - 判定「这次失败值不值得重试」的**第一道**依据始终是 Host 自己给的 `result.error.retryable` 标志;文本匹配只是补上 Host 判不出来的那一类 ### 4. 界面不变 因为**有上限**,原来的 `live.retrying` 文案(带 `n/max`)继续适用, 不需要新增文案,客户端零改动。 ## 测试 - 新增 `tests/turn-retry-backoff.test.mjs`(4 项):窗口形状、5xx/网络故障的识别、 400/404/422 不重试、正常消息不误判 - minimax-bot 全量 **121 项 / 114 通过 / 7 失败** —— 与上游 main **完全相同** (上游自己也有这 7 个失败,与本改动无关) - `npm run validate` 对本插件 **OK**、`repository: OK`;`npm test` 69/69 ## 取舍 - **没有**做「暂时性错误无限重试」:那会让失败静默持续下去,也不符合 「连续重试要设上限」的要求。现在的做法是**把窗口拉长到能覆盖一次真实抖动**, 同时**保留上限**。 - **没有**做「400/404 也重试」:与 Host 的设计意图相反。 - 判定仍然**优先**用 Host 的 `retryable` 标志与 `reason`;文本匹配只是补位, 不是另起一套分类。
MyPrototypeWhat
left a comment
There was a problem hiding this comment.
REQUEST_CHANGES(head 7e672a3)
方向对:把回合重试从 2 档拉长到 10 档(约 8 分钟、仍有上限),并用收窄的文本匹配补 Host 对通用 5xx 的 fail-closed,同时明确不把 400/404/422 当成暂时性错误。范围也清楚,只动官方 minimax-bot 的重试逻辑。
合并前请先修这一处(阻塞):
miniapp/node/engine.mjs:message.live的max用了this.retryMaxAttempts + 1,但构造函数里从未赋值retryMaxAttempts(只有一句过期的「无限重试」注释)。重试时界面会变成{n}/NaN,和 PR 里「live.retrying带 n/max、客户端零改动」的说法不符。上限实际已经由attempt < this.retryDelays.length管住了,这里应改成this.retryDelays.length + 1(或真正定义并维护retryMaxAttempts),并删掉/改掉仍写「无限重试」「TURN_RETRY_MAX_ATTEMPTS」的过期注释。
建议一并处理(不阻塞策略门禁,但会影响发版):
package.json仍是0.7.4,.minimax-plugin/plugin.json已是0.7.5;mcp.mjs的serverInfo.version还是0.7.0。请与 #48 协调版本号(两边都在抢0.7.5),后合的一方 rebase 后升到下一个版本。
已核对
- 400/404/422 不重试(新匹配路径):
isTransientErrorMessage不含这些状态;新增测试覆盖「HTTP 400/404」「code=422」以及「用了 400 tokens」类误判。既有canRetry仍尊重 Host 的retryable === false与NO_RETRY_REASONS。 - 有上限:
attempt < this.retryDelays.length,默认 10 档;注入retryDelaysMs: []仍表示不重试。 - 安全:diff 不涉及凭据、宿主数据库、其它插件数据;无
allowOnce/ 模型自动同意。 - 与 #48:同插件、不同关注点(重试 vs 气泡/操作栏/任务条),可以保持拆分。硬冲突主要在
README.md(两边都改);plugin.json两边都是0.7.4→0.7.5(内容相同,合并时仍可能撞版本语义)。engine.mjs仅本 PR 改,无代码冲突。后合的人 rebase 并协调版本即可。
测试(本机,Asia/Shanghai)
npm run check:plugins/minimax-ai/minimax-bot: OK,repository: OK- 新增
tests/turn-retry-backoff.test.mjs:4/4 通过 node --test tests/*.mjs(先在tests/下npm ci装 jsdom):204/204 通过(相对 main 的 200 条多出本 PR 的 4 条)- 仓库根
node --test test/*.test.mjs:69/69 通过 - 未装 jsdom 时会有 7 个 UI 相关文件直接加载失败,与 main 相同,与本改动无关
Standards(摘要)
- 符合 AGENTS 门禁:
npm run check通过;无宿主/用户数据破坏面。 - 版本三处不一致(plugin.json / package.json / mcp serverInfo)违反「manifest 一致」惯例。
retryMaxAttempts/TURN_RETRY_MAX_ATTEMPTS属于 Mysterious Name / 半截重构残留。
Spec(摘要)
- PR 正文要求的窗口形状、上限、400/404/422 不进暂时性匹配、窄匹配:已实现并有单测。
- 「界面不变、n/max 继续适用」因
retryMaxAttempts未定义而未兑现 → 对应上面的阻塞项。 - 注释仍残留「暂时性 ⇒ 无限重试」叙事,与最终有上限的实现不一致(清理即可)。
| s: Math.round(delay / 1000), | ||
| n: attempt + 1, | ||
| // 有次数上限 ⇒ 如实告诉用户还剩几次机会。 | ||
| max: this.retryMaxAttempts + 1, |
There was a problem hiding this comment.
阻塞:this.retryMaxAttempts 从未赋值(构造函数里只有过期注释),这里会得到 NaN,live.retrying 的 {n}/{max} 会显示成 2/NaN。上限已经由上面的 attempt < this.retryDelays.length 保证,请改成 this.retryDelays.length + 1,或真正定义并维护 retryMaxAttempts,并清掉仍写「无限重试」/TURN_RETRY_MAX_ATTEMPTS 的注释。
There was a problem hiding this comment.
已修,谢谢指出。
原因是我收敛重试逻辑时,删掉了 this.retryMaxAttempts 的赋值却漏改了用法,于是 undefined + 1 = NaN。
按你的建议改成 this.retryDelays.length + 1(与上游原本的写法一致),现在 live.retrying 显示的是 n/11(10 次重试 + 首次)。
另外两条也一并处理了:
- 过期注释:还写着「无限重试」的地方已改掉 —— 现在统一口径是「暂时性错误同样受次数上限约束」。
- 版本号:
plugin.json/package.json/mcp.mjs的serverInfo.version三处之前是 0.7.5 / 0.7.4 / 0.7.0,现已统一为 0.7.5(与 fix(minimax-bot): fold the earlier bubbles of a long reply, bottom-left reply actions #48 撞号这点我在 PR 描述里也标了一句,最终顺序以维护者为准)。
你那边跑到的 204 个测试,我这边重跑也是 121 项 / 114 通过 / 7 失败 —— 那 7 个与上游 main 完全相同(上游自己也有),与本 PR 无关。
维护者 review 指出的问题: 1. **`max` 显示成 `2/NaN`**(真 bug,我引入的): 删掉 `this.retryMaxAttempts` 的赋值时漏改了**用法**, `this.retryMaxAttempts + 1` ⇒ `undefined + 1` = NaN。 ⇒ 改用 `this.retryDelays.length + 1`(与上游原本的写法一致)。 2. **清掉还写着「无限重试」的旧注释** —— 现在统一口径: 暂时性错误**同样受次数上限约束**(第 10 次之后把原始错误暴露给用户)。 3. **版本号三处对齐 0.7.5**:`plugin.json` / `package.json` / `mcp.mjs` 的 `serverInfo.version` 之前分别是 0.7.5 / 0.7.4 / 0.7.0。 (与 MiniMax-AI#48 的 0.7.5 撞号是预期的,两边都需要维护者定顺序。) 测试:121 项 / 114 通过 / 7 失败 —— 与上游 main 完全相同(那 7 个是上游自己的失败)。
按你的意见改了
关于版本号撞 #48:我这边就先用 0.7.5,哪个 PR 先合、另一个就 rebase 改成 0.7.6,以维护者为准。 测试:121 项 / 114 通过 / 7 失败 —— 与上游 main 完全相同(那 7 个上游自己也有,与本 PR 无关)。你那边跑到的 204 个也全过。 |
MyPrototypeWhat
left a comment
There was a problem hiding this comment.
感谢更新!上一轮的阻塞问题已经修好了,复核结果如下:
已解决
max: this.retryDelays.length + 1修复正确:this.retryMaxAttempts已无任何引用;attempt计的是重试次数,n = attempt + 1在默认配置下从 2 走到 11,与总尝试次数(首次 1 次 + 重试 10 次)= 11 一致,不再出现n/NaN,也没有差一错误。注入retryDelaysMs: []时根本不会进入重试分支,不会显示奇怪的x/1。- 「无限重试」的过期注释已清理。
- 三处版本号已对齐。
需要修改(阻塞)
- main 已经通过 #48 发布了 minimax-bot 0.7.5(main 当前为 0c187f3)。本 PR 基于更早的 9804030,合入后三处版本号会和 main 完全相同,等于这次重试改动没有新版本号、用户侧收不到更新。另外 #50 也计划用 0.7.6;请 rebase 到最新 main,并在 rebase 时 main 上的版本号基础上再升一号(如果 #50 先合就是 0.7.7),三处(
package.json、.minimax-plugin/plugin.json、mcp.mjs的serverInfo.version)保持一致。- 我本地试合并过:没有实际冲突(README.md 自动合并成功),合并后
npm run check通过,插件测试 211/211 通过。所以 rebase 应该很顺利。
- 我本地试合并过:没有实际冲突(README.md 自动合并成功),合并后
非阻塞小建议(可顺手改)
engine.mjs中还有一处注释引用了不存在的TURN_RETRY_MAX_ATTEMPTS。- 总窗口时长说法不一致:文件头和测试标题写「约 8 分钟」,第 652 行注释和测试文件头注释写「约 7.5 分钟」、「60s×6」;实际是 8+20+40+60×7 = 488 秒 ≈ 8 分钟,60s 档是 7 个。
this.retryDelays下面那行/** 重试窗口的退避基数与上限… */现在没有对应字段,成了悬空注释,建议删掉或合进上一行注释。
测试(Node 22 npm run check;Node 20 node --test tests)
- PR head:check 通过;插件测试 204/204 通过
- main 基线:check 通过;207/207 通过(数量差异来自 #48 新增的测试,PR 尚未包含)
- PR 合入 main 后:check 通过;211/211 通过
| { | ||
| "name": "minimax-bot", | ||
| "version": "0.7.4", | ||
| "version": "0.7.5", |
There was a problem hiding this comment.
请 rebase 后在 main 当时的版本号上再升一号,与另外两处保持一致。
| "name": "minimax-bot", | ||
| "displayName": "MiniMax Bot", | ||
| "version": "0.7.4", | ||
| "version": "0.7.5", |
There was a problem hiding this comment.
请 rebase 后在 main 当时的版本号上再升一号,与另外两处保持一致。
| protocolVersion: PROTOCOL_VERSIONS.includes(requested) ? requested : PROTOCOL_VERSIONS[0], | ||
| capabilities: { tools: { listChanged: false } }, | ||
| serverInfo: { name: 'minimax-bot', version: '0.7.0' }, | ||
| serverInfo: { name: 'minimax-bot', version: '0.7.5' }, |
There was a problem hiding this comment.
请 rebase 后在 main 当时的版本号上再升一号,与另外两处保持一致。
| // 抛异常 ⇒ rawFailureMessage 有内容 ⇒ transient 为真 ⇒ 可重试。 | ||
| // ⚠️ 「暂时性」**不再**用来绕过下面两道判据: | ||
| // - 不再绕过 `NO_RETRY_REASONS`(auth / quota / invalid_request 等) | ||
| // - 不再绕过次数上限(见 `TURN_RETRY_MAX_ATTEMPTS`) |
There was a problem hiding this comment.
(非阻塞)这里引用的 TURN_RETRY_MAX_ATTEMPTS 并不存在,上限现在就是 this.retryDelays.length,建议改一下注释。
| !input.answer && | ||
| !toolStarted && | ||
| // ⚠️ **有上限**(2026-10-20 收敛):暂时性错误同样受次数上限约束。 | ||
| // 次数上限就是 `this.retryDelays.length`(默认 10 档 ⇒ 约 7.5 分钟窗口), |
There was a problem hiding this comment.
(非阻塞)「约 7.5 分钟」与文件头的「约 8 分钟」不一致;默认窗口合计 488 秒,建议统一为「约 8 分钟」(测试文件头注释里的「60s×6,约 7.5 分钟」同理,实际是 60s×7)。
|
提示一下合并前需要处理的两件事(这个 PR 现在和 main 冲突,麻烦 rebase 一下): 1. 冲突在 4 个文件: 2. 版本号要重新对:这个分支写的是 0.7.5,但 main 现在已经是 0.7.6,直接合进去会把版本号往回退。建议等 #52 合并后 rebase 到最新 main,再把版本三处按当时的下一个版本号写(也就是 0.7.8)。如果现在就 rebase,版本要先写 0.7.7,等 #52 合进来还得再升一次。 rebase 之后麻烦再跑一遍 |
fix(minimax-bot): 回合重试从 2 次延长到 10 档(仍有上限),400/404/422 不重试
要解决的问题:一次 30 秒的服务端抖动就让 Bot 死掉
上游 0.7.4 的重试窗口是
TURN_RETRY_DELAYS_MS = [8000, 20000]——只重试 2 次(等 8 秒、等 20 秒),然后就放弃。
而 Host 的
toLLMRetryDecision()对通用 5xx 故意 fail-closed:LLM_UPSTREAM_ERROR(50113)是「400 及以上的兜底桶」,把 HTTP 400/404 这种重试无意义的错误和 HTTP 500 这种暂时性错误混在一起,
于是分类器只能整体判成不可重试。
两条合起来的后果:
一次持续 30 秒的服务端抖动(
Internal server error (500)/502 Bad Gateway/net::ERR_NETWORK_CHANGED/ 限流 / 模型过载)就会让回合第一次失败就死掉,只能靠人手点一下才回来。Bot 团队的可用性因此被一次瞬时故障决定。
复现场景
bot 停在原地,需要人工再点一次。
改动
1. 重试窗口从 2 档延长到 10 档(仍然有上限)
总窗口约 8 分钟,足以自愈绝大多数瞬时抖动。
retryDelaysMs: []依然表示「完全不重试」,既有语义与既有测试一字未改(
engine-reliability.test.mjs51/51 通过)。2. 400 / 404 / 422 不重试
重试它们只是把同一次拒绝重复十遍。Host 自己的注释说得很清楚:
would turn an HTTP 400 or 404 into ten identical attempts。
3. 错误文本匹配收窄
只认明确的服务端 / 网络故障形态:
result.error.retryable标志;文本匹配只是补上 Host 判不出来的那一类4. 界面不变
因为有上限,原来的
live.retrying文案(带n/max)继续适用,不需要新增文案,客户端零改动。
测试
tests/turn-retry-backoff.test.mjs(4 项):窗口形状、5xx/网络故障的识别、400/404/422 不重试、正常消息不误判
(上游自己也有这 7 个失败,与本改动无关)
npm run validate对本插件 OK、repository: OK;npm test69/69取舍
「连续重试要设上限」的要求。现在的做法是把窗口拉长到能覆盖一次真实抖动,
同时保留上限。
retryable标志与reason;文本匹配只是补位,不是另起一套分类。
Need help on this PR? Tag
@codesmith-botwith what you need.